<?php
/**
 * Copyright 2013 Andrey Tushev and Probasis 
 */
namespace Emos\Editor;

use Emos\Common;

class Tree {
    public $db;
    protected $entities = array();
    public $entitiesDict = array();
        
    public $name = null;
    
    protected $reply = array();
         
    protected $inited = false;
            
    protected $mainHeight = null;
    
    public function setMainHeight($mainHeight=null) {
        $this->mainHeight = $mainHeight;
    }
            
    function setDb($db) {
        $this->db = $db;
    }
    
    function addEntity(Entity $entity) {
        $this->entities[] = $entity;        
    }
    
    function init() {
        foreach($this->entities as $entity) {
            $entity->tree = $this;
            $entity->init();
        }
        
        if($this->name === null) {
            $this->name = '';
            foreach($this->entitiesDict as $entity) {
                $this->name .= 
                    $entity->getName().':'.
                    $entity->pk . ':' . 
                    implode(',', $entity->fkes).';';
            }
            $this->name = md5($this->name);            
        }
        
        $this->inited = true;
    }
    
    protected function checkNodeAccess($entityName, $pkValue) {        
    }
    
    protected function setCurrentNode($entityName, $pkValue, $sub=true) {
        $entity = $this->getEntityByName($entityName);
        $_SESSION['emos'][$this->name]['currentNode'] = array(
            'entityName'  => $entityName, 
            'pkValue'     => $pkValue,
            'sub'         => !!$sub
        );
    }
  
    public function getCurrentNode() {
        $cn = @$_SESSION['emos'][$this->name]['currentNode']; 
        
        if($cn) {            
            $entity = $this->getEntityByName($cn['entityName']);            
            return new Node($this, $entity, $cn['pkValue'], $cn['sub']);
        }
        else {
            return null;
        }        
    }
                
    protected function initAction() {
        try {                                
            $this->reply['toolbarHtml'] = $this->getToolbarHtml();        
            $this->reply['listHtml'] = $this->getListHtml();
            $this->reply['mainHeight'] = $this->mainHeight;
        }
        catch(\Exception $ex) {
            unset($_SESSION['emos'][$this->name]['currentNode']);
            unset($_SESSION['emos'][$this->name]['openedBranches']);            
            throw $ex;
        }
        //$this->reply['formHtml'] = $entity->getFormHtml();        
    }

    protected function closeAllBranchesAction() {
        unset($_SESSION['emos'][$this->name]['openedBranches']);
                
        $this->reply['toolbarHtml'] = $this->getToolbarHtml();
        $this->reply['listHtml'] = $this->getListHtml();
        $this->reply['formHtml'] = ''; 
    }
    
    protected function selectNodeAction($entityName, $pkValue, $sub) {
        $entity = $this->getEntityByName($entityName);        
        
        $this->checkNodeAccess($entity, $pkValue);
        
        $sub = ($sub == 'true');
        $this->setCurrentNode($entityName, $pkValue, $sub);         
        
        $this->reply['toolbarHtml'] = $this->getToolbarHtml();
        $this->reply['breadcrumbsHtml'] =  $this->getBreadcrumbsHtml();        
        $this->reply['listHtml'] =  $this->getListHtml();
        $this->reply['formHtml'] = $sub ? '' : $entity->getFormHtml($pkValue);
    }      
  
    protected function getNewItemFormAction($newEntityName, $parentEntityName=null, $parentPkValue=null) {        
        $parentNode = null;
        if(($parentEntityName != null) && ($parentPkValue != null)) {
            $parentNode = new Node($this, $this->getEntityByName($parentEntityName), $parentPkValue);        
        }
        
        $newEntity = $this->getEntityByName($newEntityName);
        
        $this->reply['formHtml'] = $newEntity->getFormHtml(null, $parentNode);
    }    
    
    protected function openBranchAction($entityName, $pkValue) {     
        $entity = $this->getEntityByName($entityName);                           
                
        $entity->open($pkValue);            
        $this->reply['listHtml'] = $this->getListHtml();          
    }
    
    protected function closeBranchAction($entityName, $pkValue) {
        $entity = $this->getEntityByName($entityName);            
        $entity->close($pkValue);            
        $this->reply['listHtml'] = $this->getListHtml();         
    }         

    // TODO: fk, limit optimization
    protected function moveAction($position) {
        $node = $this->getCurrentNode();
        if($node) {
            $moveField = $node->getEntity()->getMoveField();
            if(!$moveField) {
                throw new \Exception('No movable field');
            }                       

            $whereSql = $node->getEntity()->getWhere();
            
            // Current item
            $curSort = $this->db->selectVal("SELECT ?sortName:name FROM ?table:name WHERE ?pkName:name = ?pkValue ORDER BY sort", array(
                'table'   => $node->getEntity()->table,
                'pkName'  => $node->getEntity()->pk,
                'pkValue' => $node->getPkValue(),
                'whereSql'=> $whereSql,
                'sortName'=> $moveField
            ));                        
            
            $fkWhereSql = "1=1";
            $breadcrumbs = $this->getBreadcrumbs(2);
            if($breadcrumbs->getLength() > 1) {
                $fkName  = $breadcrumbs->getNodeFromEnd(0)->getFkName();
                $fkValue = $breadcrumbs->getNodeFromEnd(1)->getPkValue();
                $fkWhereSql = $this->db->nameEscape($fkName) . "='" . $fkValue . "'";
            }
            else {
                $fkWhereSql = '1=1';
                $entity  = $breadcrumbs->getNode(0)->getEntity();
                foreach($entity->fkes as $fk) {            
                    $fkWhereSql .= " AND ";                        
                    $fkWhereSql .= "(".$this->db->nameEscape($fk)."=0 OR ".$this->db->nameEscape($fk)." IS NULL)";
                }                                
            }
            
            // Find destination
            if($position == 'up' || $position == 'down') {
                $row = $this->db->selectRow("SELECT ?pkName:name AS pk_value, ?sortName:name AS sort_value FROM ?table:name WHERE (?fkWhereSql:raw) AND (?whereSql:raw) AND sort ?compare:raw ?curSort ORDER BY ?sortName:name ?dir:raw", array(
                    'table'         => $node->getEntity()->table,
                    'pkName'        => $node->getEntity()->pk,
                    'pkValue'       => $node->getPkValue(),
                    'curSort'       => $curSort,
                    'compare'       => ($position == 'up') ? '<' : '>',
                    'dir'           => ($position == 'up') ? 'DESC' : 'ASC',
                    'whereSql'      => $whereSql,
                    'fkWhereSql'    => $fkWhereSql,
                    'sortName'      => $moveField
                ));            
            }
            else if($position == 'top' || $position == 'bottom') {
                $row = $this->db->selectRow("SELECT ?pkName:name AS pk_value, ?sortName:name AS sort_value FROM ?table:name WHERE (?fkWhereSql:raw) AND (?whereSql:raw) ORDER BY ?sortName:name ?dir:raw", array(
                    'table'         => $node->getEntity()->table,
                    'pkName'        => $node->getEntity()->pk,
                    'pkValue'       => $node->getPkValue(),
                    'curSort'       => $curSort,                    
                    'dir'           => ($position == 'bottom') ? 'DESC' : 'ASC',
                    'whereSql'      => $whereSql,
                    'fkWhereSql'    => $fkWhereSql,
                    'sortName'      => $moveField
                ));                 
            }
            
            // Update both records
            if($row) {                                
                $this->db->exec("UPDATE ?table:name SET ?sortName:name=?sort WHERE ?pkName:name=?pkValue", array(
                    'table'   => $node->getEntity()->table,
                    'pkName'  => $node->getEntity()->pk,
                    'pkValue' => $node->getPkValue(),       
                    'sort'    => $row['sort_value'],
                    'sortName'=> $moveField
                ));
                $this->db->exec("UPDATE ?table:name SET ?sortName:name=?sort WHERE ?pkName:name=?pkValue", array(
                    'table'   => $node->getEntity()->table,
                    'pkName'  => $node->getEntity()->pk,
                    'pkValue' => $row['pk_value'],       
                    'sort'    => $curSort,
                    'sortName'=> $moveField
                ));                      
            } 
            else {
                $this->addMessage(Common::t('no way to move'), 'error');
            }
        }
        
        $this->reply['listHtml'] = $this->getListHtml();         
    }    
    
    protected function insertAction($newEntityName, $parentEntityName=null, $parentPkValue=null) {
        $parentNode = null;
        if(($parentEntityName != null) && ($parentPkValue != null)) {
            $parentNode = new Node($this, $this->getEntityByName($parentEntityName), $parentPkValue);        
        }        
                
        $newEntity    = $this->getEntityByName($newEntityName);  
        
        if($parentEntityName) {
            $parentEntity = $this->getEntityByName($parentEntityName);  
            $this->checkNodeAccess($parentEntity, $parentPkValue);
        }
                
        if(!$newEntity->validate()) {        
            $this->reply['invalidFields'] = $newEntity->getFieldsErrors();            
            $this->addMessage(Common::t('incorrect values'), 'error');            
        }
        else {        
            $newPkValue = $newEntity->insert($parentNode);         
            $this->reply['newPkValue'] = $newPkValue;

            $this->addMessage(Common::t('record added'));            
        }               
    }    
    
    protected function updateAction($entityName, $pkValue) {
        $entity = $this->getEntityByName($entityName);      
        $this->checkNodeAccess($entity, $pkValue);
        
        if(!$entity->validate()) {        
            $this->reply['invalidFields'] = $entity->getFieldsErrors();            
            $this->addMessage(Common::t('incorrect values'), 'error');            
        }
        else {        
            $entity->update($pkValue);  
            $this->addMessage(Common::t('record updated'));
        }
    }    
    
    protected function deleteAction() {
        $cn = $this->getCurrentNode();
        $entity = $cn->getEntity();                    
        $pkValue = $cn->getPkValue();               
        
        $this->checkNodeAccess($entity, $pkValue);
                
        $n = $entity->getNestedRecordsNum($pkValue);
        if($n == 0) {            
            $entity->delete($pkValue);
            // TODO: select parent                    
                    
            $this->reply['toolbarHtml'] = $this->getToolbarHtml();         
            $this->reply['listHtml'] = $this->getListHtml();
            $this->reply['formHtml'] = '';            
            
            $this->addMessage(Common::t('record deleted'));
        }
        else {            
            $this->addMessage(Common::t('can not delete, nested items', array('nested'=>$n)), 'error');
        }          
    }    
    
    protected function updateSchemaAction() {
        if(!Common::$developerMode) {
            throw new \Exception("Not developer mode");
        }          
        
        
        $struct = array();
        foreach($this->entitiesDict as $entity) {
            $struct['tables'][] = $entity->getDbStructure();
        }
        
        $this->db->updateDbSchema($struct);       
        
        $this->addMessage('Database schema has been updated');
    }    
    
    protected function addFakeAction() {
        if(!Common::$developerMode) {
            throw new \Exception("Not developer mode");
        }          
        
        $fkName = null;
        $fkValue = null;
        $path = $this->getBreadcrumbs();        
        if($path->getLength() > 1) {
            $fkName  = $path->getNodeFromEnd(0)->getFkName();
            $fkValue = $path->getNodeFromEnd(1)->getPkValue();                
        }        
        
        $n = 0;        
        foreach($this->entitiesDict as $entity) {
            $n += $entity->addFake($fkName, $fkValue);
        }
        
        $this->reply['listHtml'] = $this->getListHtml();
        $this->addMessage("Added $n fake records");
    }      
    
    public function ajax() {                
        if(!$this->inited) {
            $this->init();
        }
        
        ob_start();
        
        $this->reply['source'] = 'emos';        
        $this->reply['name'] = $this->name;        
        $this->reply['messagesHtml'] = '';       
        
        
        $action = @$_POST['action'];
        try {
            $methodName = "{$action}Action";
            if(method_exists($this, $methodName)) {                  
                $r = new \ReflectionMethod($this, $methodName);
                $params = array();
                foreach($r->getParameters() as $param) {
                    $name = $param->getName();
                    if(isset($_POST[$name])) {
                       $params[$name] = $_POST[$name];
                    }
                    else {
                        if($param->isDefaultValueAvailable()) {
                            $params[$name] = $param->getDefaultValue();
                        }
                        else {                        
                            throw new \Exception("No obligated parameter '$name' for '$action' action.");
                        }
                    }                
                }            
                                       
                call_user_func_array(array($this, $methodName), $params);                
            }
            else {
                throw new \Exception("Bad action '$action'");
            }
        }
        catch(\Exception $ex) {  
            $this->addMessage(Common::t('exception', array('message'=>Common::plain($ex->getMessage()))), 'error');
            $this->reply['exception'] = $ex->getMessage();
        }
        
        $output = ob_get_clean();
        if($output != '') {
            $this->addMessage('<p><b>PHP output:</b></p> <div class="output">'.Common::plain($output).'</div></pre>');            
        }
        
        header("Content-type: application/json");
        if(defined('JSON_PRETTY_PRINT') && defined('JSON_UNESCAPED_UNICODE')) {
            print json_encode($this->reply, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);        
        }
        else {
            print json_encode($this->reply);        
        }
    }
    
    protected function getListHtml() {
        $listHtml = '';
        foreach($this->entities as $entity) {
            $listHtml .= $entity->getListHtml();
        }
        return $listHtml;
    }
    
    protected static function makeButton($title, $class, $data=null) {
        $html = "<div class='button {$class}'";
        if($data) {
            foreach($data as $n => $v) {            
                $html .= " data-{$n}='".Common::plain($v)."'";
            }
        }
        $html .= "><span class='glyph'></span> ".Common::plain($title)."</div>";
        return $html;
    }
    
    protected function getToolbarHtml() {
        $cn = $this->getCurrentNode();                
        
        $groups = array();            
        
        try {
            // Insert into a parent
            if($cn) {            
                $breadcrumbs = $this->getBreadcrumbs(2)->getNodes();           
                $p = count($breadcrumbs)-1  - ($cn->isSub() ? 0 : 1);

                if(isset($breadcrumbs[$p])) {            
                    $possibleEntities = $breadcrumbs[$p]->getEntity()->entities;            
                    foreach($possibleEntities as $e) {                
                        $entity = $e['entity'];
                            if($entity->allowInsert) {
                            $parentPkValue = $breadcrumbs[$p]->getPkValue();                    
                            $groups['addAndDelete'][] = array('title'=>Common::t('create', array('title'=>$entity->getTitle())), 'class'=>'new', 'data'=>array(
                                'parentEntityName'=>$breadcrumbs[$p]->getEntityName(), 
                                'parentPkValue'=>$parentPkValue, 
                                'newEntityName'=>$entity->name
                            ));
                        }
                    }
                }
                else {
                    // Add into root entities
                    foreach($this->entities as $entity) {         
                        if($entity->allowInsert) {
                            $groups['addAndDelete'][] = array('title'=>Common::t('create', array('title'=>$entity->getTitle())), 'class'=>'new', 'data'=>array('newEntityName'=>$entity->name));
                        }
                    }
                }        
            }
            // Add into root (Nothing selected)
            else {
                foreach($this->entities as $entity) {   
                    if($entity->allowInsert) {
                        $groups['addAndDelete'][] = array('title'=>Common::t('create', array('title'=>$entity->getTitle())), 'class'=>'new', 'data'=>array('newEntityName'=>$entity->name));
                    }
                }            
            }                
            
            if($cn && !$cn->isSub()) {   
                if($cn->getEntity()->allowDelete) {
                    $groups['addAndDelete'][] = array('title'=>Common::t('delete'), 'class'=>'delete');
                }
            }                

//            $groups['copy'][] = array('title'=>'Duplicate', 'class'=>'');
//            $groups['copy'][] = array('title'=>'Cut', 'class'=>'');
//            $groups['copy'][] = array('title'=>'Copy', 'class'=>'');
//            $groups['copy'][] = array('title'=>'Paste', 'class'=>'');

            if($cn && !$cn->isSub() && ($cn->getEntity()->getMoveField() !== null)) {            
                $groups['move'][] = array('title'=>Common::t('move up'), 'class'=>'moveUp');
                $groups['move'][] = array('title'=>Common::t('move down'), 'class'=>'moveDown');
                $groups['move'][] = array('title'=>Common::t('move to top'), 'class'=>'moveTop'); 
                $groups['move'][] = array('title'=>Common::t('move to bottom'), 'class'=>'moveBottom');       
            }

            $groups['branches'][] = array('title'=>Common::t('close all'), 'class'=>'closeAllBranches');
            //$groups['branches'][] = array('title'=>'Open branch', 'class'=>'');                           
        }
        // Bad database structure
        catch(\Exception $ex) {
            print $ex->getMessage();            
        }     
        
        if(Common::$developerMode) {
            $groups['dev'][] = array('title'=>'Update Schema', 'class'=>'updateSchema');
            $groups['dev'][] = array('title'=>'Add Fake', 'class'=>'addFake');
        }
                
        $html = '';
        foreach($groups as $group) {
            $html .= "<div class='group'>";
            foreach($group as $e) {
                $html .= self::makeButton($e['title'], @$e['class'], @$e['data']);
            }
            $html .= "</div>";
        }       
        
        return $html;
    }
    
    public function getBreadcrumbsHtml() {
        $steps = $this->getBreadcrumbs()->getNodes();
        $html = '';
        foreach($steps as $s) {            
            $stepHtml = $s->getEntity()->makeBreadcrumbsItemHtml($s->getRow(), $s->getPkValue());            
            if($html != '') {
                $html .= "<span class='stepSep'> &rarr; </span>";
            }
            $html .= "<span class='link' data-entity='".Common::plain($s->getEntityName())."' data-pk='".Common::plain($s->getPkValue())."'>{$stepHtml}</span>";
        }
        return $html;
    }
    
    public function getBreadcrumbs($maxSteps=1000000) {
        static $infinity = 50;

        $node = $this->getCurrentNode();
        $currentEntity = $node->getEntity();                    
        $currentPkValue = $node->getPkValue();  
        $currentRow = $currentEntity->getRowByPk($currentPkValue);
        $currentFkName = false;
        
        $steps = array();                
        for($n=0; $n<$infinity; $n++) {            
            $node = new Node($this, $currentEntity, $currentPkValue);
            $node->setRow($currentRow);
            $node->setFkName($currentFkName);
            $steps[] = $node;            
            
            $p = $currentEntity->getParentItem($currentPkValue);
            if(($p === null) || ($n >= $maxSteps)) {
                $breadcumbs = new Path();
                for($c = count($steps)-1; $c>=0; $c--) {
                    $breadcumbs->addNode($steps[$c]);
                }                
                return $breadcumbs;
            }            
            else {
                $currentEntity  = $p['entity'];                    
                $currentPkValue = $p['pkValue'];            
                $currentRow     = $p['row'];  
                
                $node->setFkName($p['childFkName']);            
            }            
        }
        throw new \Exception("Too long loop in structure ($infinity steps)");
    }    

    public function getEntityByName($entityName) {
        $entity = @$this->entitiesDict[$entityName];
        if($entity) {
            return $entity;
        }
        
        throw new \Exception("Incorrect entity name '{$entityName}'");
    }
    
    public function addMessage($message, $type='notice') {
        $this->reply['messagesHtml'] .= "<div class='message {$type}'>{$message}</div>";
    }
}